# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Name:        chunk_main.py
# Purpose:     GAEDrive: Chunk Server
#
# Author:      Crane Chu <cranechu (at) gmail.org>
#
# Created:     Nov. 20, 2009
# Copyright:   Copyright 2009,2010 Crane Chu
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#-------------------------------------------------------------------------------
#!/usr/bin/env python

__author__ = 'Crane Chu'

# MODULES
from google.appengine.api.labs import taskqueue
from google.appengine.api import urlfetch
from google.appengine.ext import webapp
from django.utils import simplejson
import wsgiref.handlers
import urllib
import time
from common import *


# internal functions
def _master_server_url(host):
    return "localhost:8080" if 'localhost' in host \
            else "gaestorage0.appspot.com"    
        
def _delete_chunks(chunk_keys):
    """ put a list of chunks to the background delete task
    
    Args:
        chunk_keys: a list of chunks' key
    
    Returns:
        True. Just ignore
    """
    # add to background task of deleting chunks
    # 8 chunks per task is enough to release space
    key_list = []
    for key in chunk_keys:
        key_list.append(key)
        if len(key_list) >= 8:
            taskqueue.add(url = '/bg_delete_chunks',
                          params = {'keys': simplejson.dumps(key_list)})
            key_list = []
    
    if key_list:
        taskqueue.add(url = '/bg_delete_chunks',
                      params = {'keys': simplejson.dumps(key_list)})
        key_list = []
        
    return True


class initChunkServer(webapp.RequestHandler):
    """ init the chunk server through visiting following page in browser: 
    https://chunk-server-id.appspot.com/format
    
    Args:
        NA
        
    Returns:
        404 if any error
    """
    def get(self):
        chunk_url = self.request.host
        MASTER_SERVER_URL = _master_server_url(chunk_url)
        url = URLHEAD + MASTER_SERVER_URL + "/init_chunkserver"
        # not to report the total space. avoid too full utilizations
        data = {"url": chunk_url, "space": str(CHUNK_SERVER_SPACE / 16 * 15)}
        result = urlfetch.fetch(url, urllib.urlencode(data), urlfetch.POST)
        if result.status_code == 200:
            # MAX_FETCH_COUNT should not be the problem. GAE is fixing it ...
            chunks = Chunk.all().fetch(MAX_FETCH_COUNT)
            _delete_chunks([str(chunk.key()) for chunk in chunks])
            
            self.response.out.write("OK, %s initialized..." % self.request.host)
        else:
            self.error(404)


class deleteChunk_bg(webapp.RequestHandler):
    """ background delete task
    
    Args:
        chunks: the list of chunks to be deleted
        
    Returns:
        NA
    """
    def post(self):
        chunk_keys = self.request.get('keys')
        # double check with master if the chunks are delete
        MASTER_SERVER_URL = _master_server_url(self.request.host)
        url = URLHEAD + MASTER_SERVER_URL + "/doublecheck?func=delete"
        args = (chunk_keys,)
        payload = urllib.urlencode({'args' : simplejson.dumps(args)})
        result = urlfetch.fetch(url, payload, urlfetch.POST)
        if result.status_code == 200:
            # delete the chunks
            key_list = \
                [db.Key(key_str) for key_str in simplejson.loads(chunk_keys)]
            db.delete(key_list)


class checkChunk_bg(webapp.RequestHandler):
    """ background check chunk.
    
    Args:
        chunk_key: the key of the chunk to check
        
    Returns:
        NA
    """
    def post(self):
        chunk_key = self.request.get('chunk_key')
        # double check with master if the chunks are delete
        MASTER_SERVER_URL = _master_server_url(self.request.host)
        url = URLHEAD + MASTER_SERVER_URL + "/doublecheck?func=create"
        args = (chunk_key,)
        payload = urllib.urlencode({'args' : simplejson.dumps(args)})
        # delay 5 seconds, to let master create the chunk entity
        # GAE gives us 30 seconds in background tasks ...
        time.sleep(5)
        result = urlfetch.fetch(url, payload, urlfetch.POST)
        if result.status_code == 404:
            # delete the no exist chunk
            chunk = db.get(chunk_key)
            if chunk:
                chunk.delete()


class stubMaster(webapp.RequestHandler):
    def post(self):
        """ receive the command and dispatch
        
        Args:
            func: the func name of the command from client
            args: the arguments of the functions
            
        Returns:
            ret_str: the return value of the function in JSON
        """
        funcname = self.request.get('func')
        args = urllib.unquote(self.request.get('args'))
        
        # check the args
        funcname = funcname.lower()
        ret = False
        if not funcname.startswith('_') and funcname != 'post':
            func = getattr(self, funcname)
            args = simplejson.loads(args)
            try:
                if args:
                    ret = func(args)
                else:
                    ret = func()
            except:
                self.response.out.write(simplejson.dumps(False))
                raise
        else:
            ret = False
            
        self.response.out.write(simplejson.dumps(ret))
        
    def create(self):
        """ create one chunk, command from master
        
        Args:
            None
        
        Returns:
            404 if any error
        """
        try:
            chunk = Chunk()
            chunk.data = ''
            chunk.put()
            taskqueue.add(url = '/bg_check_chunk',
                          params = {'chunk_key': str(chunk.key())})
            return str(chunk.key())
        except:
            self.error(404)

    def delete(self, chunk_keys):
        """ delete chunks, command from master only
        
        Args:
            chunks: the list of chunks to be deleted
        
        Returns:
            404 if any error
        """
        try:
            _delete_chunks(chunk_keys)
            return True
        except:
            self.error(404)

    def truncate(self, chunk_key, length):
        """ truncate one chunk, command from client
        
        Args:
            chunk: one chunk to be truncated
            length: length of the chunk
        Returns:
            404 if any error
        """
        # double check with master
        MASTER_SERVER_URL = _master_server_url(self.request.host)
        url = URLHEAD + MASTER_SERVER_URL + "/doublecheck?func=truncate"
        args = (chunk_key, length)
        payload = urllib.urlencode({'args' : simplejson.dumps(args)})
        result = urlfetch.fetch(url, payload, urlfetch.POST)
        if result.status_code == 200:
            def _transaction(chunk_key, length):
                chunk = db.get(chunk_key)
                chunk.data = chunk.data[:length]
                chunk.put()
            db.run_in_transaction(_transaction, chunk_key, length)
            return True
        else:
            self.error(404)


class readChunk(webapp.RequestHandler):
    """ read data from chunk
    
    Args:
        chunk_key: the key of the chunk in this chunk server.
        rand: rand num assigned by master, to prevent error operation 
        
    Returns:
        data the the chunk
    """
    def post(self):
        # read _data_ from _id_ chunk
        chunk_key = self.request.get('key')
        rand = self.request.get('rand')
        # double check if the chunk has be created and can be read by fh
        MASTER_SERVER_URL = _master_server_url(self.request.host)
        url = URLHEAD + MASTER_SERVER_URL + "/doublecheck?func=read"
        args = (chunk_key, int(rand))
        payload = urllib.urlencode({'args' : simplejson.dumps(args)})
        result = urlfetch.fetch(url, payload, urlfetch.POST)
        if result.status_code == 200:
            chunk = db.get(chunk_key)
            # send it as binary data
            self.response.headers['Content-Type'] = "application/octet-stream"
            self.response.out.write(chunk.data)
        else:
            # can not find the data
            self.error(404)
        

class writeChunk(webapp.RequestHandler):
    """ write data to a chunk
    
    Args:
        id: the id of the chunk in this chunk server. Assigned by master
        data: the data of the chunk to be written
        
    Returns:
        length of data written to chunk server
        or, 404 if any Error
    """
    def post(self):
        # write _data_ to _id_ chunk
        chunk_key = self.request.get('key')
        rand = self.request.get('rand')
        # double check if the chunk has be created and can be written by rand
        MASTER_SERVER_URL = _master_server_url(self.request.host)
        url = URLHEAD + MASTER_SERVER_URL + "/doublecheck?func=write"
        args = (chunk_key, int(rand))
        payload = urllib.urlencode({'args' : simplejson.dumps(args)})
        result = urlfetch.fetch(url, payload, urlfetch.POST)
        if result.status_code == 200:
            data = self.request.get('data')
            def _transaction(chunk, data):
                chunk.data = db.Blob(data)
                chunk.put()
            db.run_in_transaction(_transaction, db.get(chunk_key), data)
            self.response.out.write(str(len(data)))
        else:
            self.error(404)
            
            
########==========main entry=============
application = webapp.WSGIApplication([
    ('/format', initChunkServer),
    ('/bg_delete_chunks', deleteChunk_bg),
    ('/bg_check_chunk', checkChunk_bg),
    ('/chunk_op', stubMaster),
    ('/write_chunk', writeChunk),
    ('/read_chunk', readChunk),
    
], debug = True)


def main():
    wsgiref.handlers.CGIHandler().run(application)

if __name__ == '__main__':
    main()
